package com.altio.swing.portlet;

import java.awt.Component;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.event.MouseMotionListener;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.swing.JComponent;
import javax.swing.JLayeredPane;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.border.EmptyBorder;

import com.altio.swing.CornerLayoutManager;
import com.altio.swing.DraggableConstraints;
import com.altio.swing.DraggableLayeredPane;
import com.altio.swing.portlet.layout.ColumnLayout;

public class PortletDock extends JLayeredPane {
  
  private static final int RESIZE_AFTER_DRAG_DELAY = 300;

  /**
   * The UI class ID string.
   */
  public static final String uiClassID = "PortletDockUI";

  /**
   * Main panel that will contains the components
   */
  private JPanel mainPane = new JPanel();
  
  /** Lays out this component so that components can be layered on top of each other */
  private CornerLayoutManager cornerLayout = new CornerLayoutManager();
  
  /** Lays out the main panel */
  private ColumnLayout columnLayout;
  
  /**
   * Mapping of components to their respective panels
   */
  private Map<Component, PortletPanel> componentsToPanels = new HashMap<Component, PortletPanel>();
  
  private Dimension minPreferredSizeDuringDrag = null;

  /**
   * Listener to add a dragged component to our drag pane.  Will be added to all
   * the PortletPanels
   */
  private MouseMotionListener initDraggingListener = new MouseMotionAdapter() {
    @Override
    public void mouseDragged(MouseEvent e) {
      Component panel = e.getComponent();

      // Stop listening for drags, the draggablelayeredpane will take care of this now
      panel.removeMouseMotionListener(this);
      glueComponent.setBounds(panel.getBounds());
      replaceComponent(panel, glueComponent);
      dragPane.addDraggableComponent(panel, 0);
      dragPane.draggingOccured(panel, e);
      dragPane.setVisible(true);
      
      minPreferredSizeDuringDrag = mainPane.getPreferredSize();
    }
  };
  
  @Override
  public Dimension getPreferredSize() {
    Dimension preferredSize = super.getPreferredSize();
    if(minPreferredSizeDuringDrag != null) {
      preferredSize.width = Math.max(minPreferredSizeDuringDrag.width, preferredSize.width);
      preferredSize.height = Math.max(minPreferredSizeDuringDrag.height, preferredSize.height);
    }
    
    return preferredSize;
  }

  /**
   * This is the component that becomes a placeholder for the currently dragged panel.
   * 
   */
  private JComponent glueComponent = new JComponent() {
    @Override
    public boolean isOpaque() {
      return false;
    }
  };

  /**
   * Panel handles the dragging of the current component
   */
  private DraggableLayeredPane dragPane = new DraggableLayeredPane(false, false) {
    @Override
    public void draggingStopped(Component comp) {
      super.draggingStopped(comp);
      
      // Put the component back on the original panel, remove the glue component
      // and hide this panel again.
      remove(comp);
      comp.addMouseMotionListener(initDraggingListener);
      replaceComponent(glueComponent, comp);
      setVisible(false);
      
      resizeAfterDragLater();
    }

    private void resizeAfterDragLater() {
      Thread newWaitThread = new Thread() {
        @Override
        public void run() {
          
          try {
            sleep(RESIZE_AFTER_DRAG_DELAY);
          }
          catch (InterruptedException e) {
            e.printStackTrace();
          }
          
          SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
              minPreferredSizeDuringDrag = null;
              PortletDock.this.revalidate();
            }
          });
        }
      };
      
      newWaitThread.start();
    }
    
    @Override
    public void draggingOccured(final Component comp, MouseEvent arg1) {
      super.draggingOccured(comp, arg1);
      
      
      // Glue not added to the panel yet?
      if(glueComponent.getParent() != mainPane || comp.getHeight() != glueComponent.getHeight()) {
        // Handle the next drag
        return;
      }
      
      int draggedOverColumn = getDraggedOverColumn(comp);
      if(draggedOverColumn >= 0) {
        // Get the component that the dragged component is most over
        List<Component> componentsInColumnWithoutGlue = new ArrayList<Component>();
        componentsInColumnWithoutGlue.addAll(columnLayout.getComponentsInColumn(draggedOverColumn));
        componentsInColumnWithoutGlue.remove(glueComponent);
        Component draggedOver = getDraggedOverComponent(comp.getBounds(), componentsInColumnWithoutGlue);
        boolean switchOccured = false;
        
        // No component found
        if(draggedOver == null && draggedOverColumn != columnLayout.getPosition(glueComponent).x) {
          // We're over an empty column or at the bottom of the column
          // Add to the end of this column
          columnLayout.appendComponentToColumn(glueComponent, draggedOverColumn);
          switchOccured = true;
        } 
        // If we're dragged over the glue component we needn't do anything
        else if(draggedOver != null) {
          boolean insertBefore = insertBefore(comp, draggedOver);
          
          switchOccured = true;
          if(insertBefore) {
            columnLayout.insertBefore(glueComponent, draggedOver);          
          } else {
            columnLayout.insertAfter(glueComponent, draggedOver);                    
          }
        }
        
        if(switchOccured) {
          mainPane.revalidate();
          
          Dimension preferredSize = PortletDock.this.getPreferredSize();
          if(preferredSize != null &&
              minPreferredSizeDuringDrag != null) {
            minPreferredSizeDuringDrag.width = Math.max(preferredSize.width, minPreferredSizeDuringDrag.width);
            minPreferredSizeDuringDrag.height = Math.max(preferredSize.height, minPreferredSizeDuringDrag.height);
          }
        }
      }
    }

    private boolean insertBefore(final Component comp, Component draggedOver) {
      // Switch around the middle of the component we're dragging over
      int draggedOverComparePoint = draggedOver.getY() + (draggedOver.getHeight() / 2);
      // Compare with the top of the dragged component
      int compComparePoint = comp.getY();
      
      
      // If the glue and the dragged component are in the same column
      if(columnLayout.getPosition(glueComponent).x == columnLayout.getPosition(draggedOver).x) {
        
        // Switch around the middle of the dragged over component and the glue
        draggedOverComparePoint = Math.min(draggedOver.getY(), glueComponent.getY());
        draggedOverComparePoint += (draggedOver.getHeight() + glueComponent.getHeight() + columnLayout.getRowSpacing()) / 2;
        
        // Compare with the middle of the dragged component
        compComparePoint += comp.getHeight() / 2;
      }

      return compComparePoint < draggedOverComparePoint;
    }

    private int getDraggedOverColumn(final Component comp) {
      int draggedOverColumn = -1;
      
      int shortestDistance = Integer.MAX_VALUE;
      int componentMidPoint = comp.getX() + (comp.getWidth() / 2);
      
      int currentPoint = columnLayout.getColumnSpacing();
      for(int i = 0; i < columnLayout.getColumnCount() + 5; i++) {
        int columnWidth = columnLayout.getColumnWidth(i);
        
        int columnMidPoint = (currentPoint + columnWidth / 2);
        int distanceFromComponent = Math.abs(columnMidPoint - componentMidPoint);
        
        if(distanceFromComponent < shortestDistance) {
          shortestDistance = distanceFromComponent;
          draggedOverColumn = i;
        }
        // Move to next column
        currentPoint += columnWidth + columnLayout.getColumnSpacing();
      }
      return draggedOverColumn;
    }
  };

  private DraggableConstraints dragConstraints;

  public PortletDock() {
    super();
    updateUI();

    setBorder(new EmptyBorder(0, 0, 0, 0));
    setLayout(cornerLayout);
    add(mainPane, new Integer(0));
    add(dragPane, new Integer(1));

    columnLayout = new ColumnLayout(mainPane);
    mainPane.setLayout(columnLayout);

    // Make both fill this component
    cornerLayout.addLayoutComponent(CornerLayoutManager.FILL, mainPane);
    cornerLayout.addLayoutComponent(CornerLayoutManager.FILL, dragPane);
    dragPane.setVisible(false);
    
    dragConstraints = new DraggableConstraints();
    dragConstraints.scrollWhenDraggingOutOfBounds = true;
    dragPane.setDraggableConstraints(dragConstraints);
  }

  /**
   * Adds a component to the dock.  The component will be added to the dock inside a PortletPanel.
   * The panel will have the caption provided.  The panel will be placed inside the column
   * of panels that takes up the least space on the y axis.
   * 
   * @param toAdd component to add
   * @param caption caption to put on the panel
   * @return the PortletPanel that the component was added to
   */
  public PortletPanel addComponentInPanel(final Component toAdd, final String caption) {
    PortletPanel newPortletPanel = new PortletPanel(toAdd);
    newPortletPanel.setCaption(caption);
    
    componentsToPanels.put(toAdd, newPortletPanel);
    mainPane.add(newPortletPanel);

    newPortletPanel.addMouseMotionListener(initDraggingListener);
    
    return newPortletPanel;
  }
  
  /**
   * Adds a component to the dock.  The component will be added to the dock inside a PortletPanel.
   * The panel will have the caption provided.  The panel will be placed inside the column
   * specified on the x axis.  It will be placed at the nearest possible position to the 
   * position specified on the y axis.  For example, if the panel is the 
   * 
   * @param toAdd component to add
   * @param caption caption to put on the panel
   * @param x Column to add panel to
   * @param y Position in column to place panel
   * @return the PortletPanel that the component was added to
   */
  public PortletPanel addComponentInPanel(final Component toAdd, final String caption, final int x, final int y) {
    PortletPanel created = addComponentInPanel(toAdd, caption);
    
    columnLayout.position(created, new Point(x, y));
    
    return created;
  }
  
  public int getDefaultColumnWidth() {
    return columnLayout.getDefaultColumnWidth();
  }

  public void setDefaultColumnWidth(int width) {
    columnLayout.setDefaultColumnWidth(width);
    revalidate();
  }

  public PortletPanel getPanelForComponent(Component child) {
    return componentsToPanels.get(child);
  }

  /**
   * Find the component whose intersection with a given rectangle has the highest area
   * @param intersect Rectangle to search for intersection with
   * @return Component whose bounds intersect most with the rectangle
   */
  private Component getDraggedOverComponent(Rectangle intersect, Iterable<Component> components) {
    Component result = null;

    int highestArea = 0;
    for(Component component : components) {
      Rectangle bounds = component.getBounds();

      Rectangle intersection = bounds.intersection(intersect);

      if(intersection.width > 0 && intersection.height > 0) {
        int area = intersection.width * intersection.height;
        if(area > highestArea) {
          result = component;
          highestArea = area;
        }
      }
    }

    return result;
  }

  /**
   * Replace one component in the mainPane with another, including it's position
   * in the layout
   * @param toReplace
   * @param replacement
   */
  private void replaceComponent(Component toReplace, Component replacement) {
    mainPane.add(replacement);
    columnLayout.insertBefore(replacement, toReplace);
    mainPane.remove(toReplace);
  }

  /**
   * Remove a PortletPanel from the layout
   * @param portlet
   */
  public void removePanel(PortletPanel portlet) {
    mainPane.remove(portlet);
    
    if(dragPane.getDragComponent() == portlet) {
      dragPane.remove(portlet);
    }
    
    componentsToPanels.remove(portlet.getContentPane());
  }
  
  /**
   * Get the position of a panel within the layout.  The x value of the return result will
   * be the column the panel is in, the y value will be the position of the panel within
   * that column.
   * 
   * @param panel panel to find position of
   * @return Position of panel, null if the panel is not in the layout
   */
  public Point getPanelPosition(final PortletPanel panel) {
    return columnLayout.getPosition(panel);
  }

  public JComponent getGlueComponent() {
    return glueComponent;
  }

  public void setGlueComponent(JComponent glueComponent) {
    this.glueComponent = glueComponent;
  }
  
  /*
   * (non-Javadoc)
   * 
   * @see javax.swing.JComponent#updateUI()
   */
  @Override
  public void updateUI() {
    if (UIManager.get(getUIClassID()) != null) {
      setUI(UIManager.getUI(this));
    } else {
      setUI(PortletDockUI.createUI(this));
    }
  }

  
  @Override
  public String getUIClassID() {
    return uiClassID;
  }
  
  public int getColumnWidth(final int columnIndex) {
    return columnLayout.getColumnWidth(columnIndex);
  }
  
  public void setColumnWidth(final int columnIndex, final int columnWidth) {
    columnLayout.setColumnWidth(columnIndex, columnWidth);
    revalidate();
  }
  
  public void setScrollWhenDragging(final boolean scrollWhenDragging) {
    dragConstraints.scrollWhenDraggingOutOfBounds = scrollWhenDragging;
  }
  
  public boolean isScrollingWhenDragging() {
    return dragConstraints.scrollWhenDraggingOutOfBounds;
  }

  public void clearColumnWidth(int columnIndex) {
    columnLayout.clearColumnWidth(columnIndex);
    revalidate();
  }

  public boolean isColumnWidthSet(int columnIndex) {
    return columnLayout.isColumnWidthSet(columnIndex);
  }
}
